宋宝华: 论一个程序员问问题的自我修养(修订版)
问问题如打麻将
麻场如战场,麻品如人品,检验一个人的最好方法就是带他去搓麻将,一叶而知秋。同样地,通过程序员问问题的情状也基本可以看出一个程序员的治学态度。如果他问问题的时候,看得出他经过深思熟虑,经过自己的debug和剖析,这个程序员多半还是一个不错的程序员,或者正在走向“不错”。反之,一眼看出,其问的问题就没有经过大脑,甚至连问题的现场都描述不清楚,这样的程序员多半在编程领域难有大的建树,很可能只是在长年累月地低水平重复建设。
一个好的程序员,不断通过解决工程问题,查阅资料,总结知识,提升认知来逐步把自己变成高手。一个不好的程序员,有点什么事情就开始到处问,也从来不深入地去自己研究,其实,就算是工作20年,水平恐怕也还是不行,因为他陷入了简单的经验式重复。高手不完全是知识面广或深,很大程度上高在其本身的认知层面,而认知层面的提高就在于他从来都是在思考。
我是一个码农,也号称是一老湿,接触的码农比较多,平生阅码农无数(哈哈,吹牛的,阅人莫道千千万,识得真心在最初)。 好的程序员,基本上,问问题的时候,给你春风拂面的感觉,让你心里生出一些敬佩;不好的程序员,很多时候问的问题,你根本无言以对,心里一万个草泥马奔涌而过。
麻品有问题
下面我们来总结一些不好的程序员问问题的时候的几个典型的“问题”。
不调试就问
之前有时候坐在office的时候,有些人问我这样的问题,“我的程序hang死了,请教一下hang死在哪里了?”我这个时候只想说,程序是你的,你都不知道哪里hang死了,我怎么就会知道?能不能拿gdb attach上去看啊?你拿gdb attach上去看,最后程序是拿不到mutex呢?还是进while(1)了呢?还是怎么的?
任何时候,问问题之前,先问自己一个问题,“我调试过没有?”没有的话,赶紧先闭嘴。
下面我们用一个最简单的例子来解释这个过程,看下面一个程序,2个线程拿2个锁,但是顺序相反,程序肯定会死锁。死锁之后,gdb attach上去看,不要问!
pthread_mutex_tmutex_1;
pthread_mutex_tmutex_2;
void *child1(void*arg)
{
while(1) {
pthread_mutex_lock(&mutex_1);
sleep(3);
pthread_mutex_lock(&mutex_2);
printf("thread 1 get running\n");
pthread_mutex_unlock(&mutex_2);
pthread_mutex_unlock(&mutex_1);
sleep(5);
}
}
void *child2(void*arg)
{
while(1) {
pthread_mutex_lock(&mutex_2);
pthread_mutex_lock(&mutex_1);
printf("thread 2 get running\n");
pthread_mutex_unlock(&mutex_1);
pthread_mutex_unlock(&mutex_2);
sleep(5);
}
}
int main(int argc,char *argv[])
{
int tid1,tid2;
pthread_mutex_init(&mutex_1,NULL);
pthread_mutex_init(&mutex_2,NULL);
pthread_create(&tid1,NULL,child1,NULL);
pthread_create(&tid2,NULL,child2,NULL);
...
}
上述程序跑着一定会死锁,之后我们gdb attach它的pid上去调试就可以分析出来它是怎么样在死锁。我们首先看到它有3个线程:
(gdb) i threads
Id Target Id Frame
3 Thread 0xb75e9b40 (LWP 15809) "a.out" ...
2 Thread 0xb7deab40 (LWP 15808) "a.out"...
* 1 Thread 0xb7deb700 (LWP 15804)"a.out"...
切到线程2,看一下backtrace:
(gdb) thread 2
[Switching to thread 2 (Thread 0xb7deab40 (LWP 15808))]
(gdb) bt
...
#4 0x08048627 in child1 (arg=0x0) at deadlock.c:12
...
这个PID为15808的线程在代码的第12行,child1()这个函数,等一个mutex,等谁呢?
(gdb) l child1
...
12 pthread_mutex_lock(&mutex_2);
...
看出代码12行在等mutex_2,看下mutex_2的owner是谁?
(gdb) p mutex_2
$1 = {__data ={__lock = 2, __count = 0, __owner = 15809, __kind = 0, ...
我们目前看出来,15808线程在等一个mutex,这个mutex目前被15809拿着。我们现在切到线程3,然后看backtrace:
(gdb)thread 3
[Switchingto thread 3 (Thread 0xb75e9b40 (LWP 15809))]
(gdb) bt
...
#4 0x08048677 in child2 (arg=0x0) at deadlock.c:24
...
同样看出15809在等一个mutex,在代码的第24行。第24行在等谁呢?
(gdb) l child2
...
24 pthread_mutex_lock(&mutex_1);
...
第24行在等mutex_1,它的owner又是谁呢?
(gdb) p mutex_1
$2 = {__data ={__lock = 2, __count = 0, __owner = 15808, __kind = 0...
由此可见,15808在等一个mutex,该mutex被15809拿着;15809在等一个mutex,该mutex被15808拿着。显然是死锁了。
上述例子非常简单,但足够说明我们调试问题的方法和思维问题的出发点。
很多时候,我们依赖于backtrace(栈回溯),比如kernel的lockup、kernel的OOPS(哎呀!)、应用软件的hang死不动。
没有回溯崩溃或hang死现场的问问题都是耍流氓!
无背景就问
任何问题,都必有它的特定背景和再现流程,不然就反证了它是一个general或者common的问题,general的问题本身不是问题,不存在需要debug的问题,基本你通过无数渠道都可以获得答案。
所以,任何时候,问问题,应该描述清楚你所有的平台背景,以及你是如何再现的,缺乏足够信息的问题,就和缺乏足够信息的报bug是一回事。比如,作为一个测试工程师,你报的bug是说手机会死机,但是没有说什么手机,什么安卓版本,哪天的release,再现的流程,这种bug的报法,属于无效bug,只会被RD直接ignore。
下面来看一段真实的对话,它突显了我霸气和焦躁的不良品质 :-)
问题情况都说不清楚的情况下,你问问题,显然就是讨骂。比如你说RAMDISK在板子上面跑CRC错误,那么你先有没有在自己电脑上面解开过,比如执行mount -o loop或者cpio提取?
如果你电脑上面自己都没有试过,都不确定RAMDISK是不是OK的,问板子还有什么意义?另外,问板子的问题,就应该描述清楚自己板子的内存大小,RAMDISK大小,存放的内存为位置,RAMDISK读出的介质如NAND/SD驱动有无问题等一系列情况,首先排除一些最基本的可能性。
骂完了我心理也有点难受了,感觉自己有点过了。但是我的好基友说,好老湿和好医生是一样的,医者父母心。上来就骂同学,肯定是招人恨啊,但是一辈子让你不修正自己的思维方式,那么岂不是更加地不负责任?
没有背景信息和再现流程的问问题都是耍流氓!
不学习就问
很多问题,你其实稍微学习一下就没有必要问了。比如,“请问,一个Linux普通进程多久被调度到?”这样的问题本来就是很吐血,认真学习后,不会问这样的问题。明确地说,不知道!装逼的说法,就是“depend on …”,依赖的东西太多。再装逼的说法,就是“一言难尽”,但这也是大实话。
普通的Linux进程为人比较善良,我们一般用nice来形容它们的优先级,nice越高,优先级越低(你越nice,就越喜欢在地铁让座,当然越坐不到座位)。普通进程的跑法,并不是nice低的一定堵着nice高的(要不然还说什么“善良”),它是按照如下公式进行:
vruntime= pruntime*NICE_0_LOAD / weight
其中NICE_0_LOAD是1024,也就是NICE是0的进程的weight。vruntime是进程的虚拟运行时间,pruntime是物理运行时间,weight是权重,权重完全由nice决定,如下表:
在RT进程都睡过去之后(有一个特例就是RT没睡也会跑普通进程,那就是RT加起来跑地实在太久太久,普通进程必须喝点汤了),Linux开始跑NORMAL的,它倾向于调度vruntime(虚拟运行时间)最小的普通进程,根据我们小学数学知识,vruntime要小,要么分子小(喜欢睡,I/O型进程,pruntime不容易长大),要么分母大(nice值低,优先级高,权重大)。这样一个简单的公式,就同时照顾了普通进程的优先级和CPU/IO消耗情况。
比如有4个普通进程,如下表,目前显然T1的vruntime最小(这是它喜欢睡的结果),然后T1被调度到。
pruntime | Weight | vruntime | |
T1 | 8 | 1024(nice=0) | 10*1024/1024=8 |
T2 | 10 | 526(nice=3) | 10*1024/526 =19 |
T3 | 20 | 1024(nice=0) | 20*1024/1024=20 |
T4 | 20 | 820(nice=1) | 20*1024/820=24 |
然后,我们假设T1被调度再执行12个pruntime,它的vruntime将增大delta*1024/weight(这里delta是12,weight是1024),于是T1的vruntime成为20,那么这个时候vruntime最小的反而是T2(为19),此后,Linux将倾向于调度T2(尽管T2的nice值大于T1,优先级低于T1,但是它的vruntime现在只有19)。
所以,普通进程的调度,是一个综合考虑你喜欢干活还是喜欢睡和你的nice值是多少的结果。鉴于此,我们去问一个普通进程的调度延迟究竟有多大,这个问题,本身意义就不是特别大,它完全取决于当前的系统里面还有谁在跑,取决于你唤醒的进程的nice和它前面喜欢不喜欢睡觉。
类似的知识性问题还有很多很多,这些问题,完全可以通过提前学习来解答。没有必要非得问。
知识性的问题,多半也很容易在网上找到资料。比如,搞不清楚PHP、node.js和Python做后端谁好,随便一百度,知乎网友回答的一大堆。如果再去问google,那会拿到更多的答案。“敏而好学,不耻下问”固然重要,但“敏而好学,耻于上问”也同样重要。
没有经过基本的学习就问问题都是耍流氓!
做一个好麻友
我的带头大哥,现在跟好朋友打麻将,都是直接支付宝付款,每局谁胡牌后,支付宝直接转账,麻品那是相当地好。然后打2个小时的牌,产生了几千上万的GDP,为国家和人民做了很大的贡献。
下面建议Linux程序员建立如下好习惯,做一个愿赌服输的好男人:
有问题,找男人
有问题,先man。“请问fread的第二个参数怎么设置?”鬼晓得?你man啊!
$ man fread
我man过了,我man open的时候出来的居然不是open(),而是openvt:
于是就要
$ man 2 open
这个时候出来的才是open,请问这个2是个什么鬼?那么,可以man一下man。
$ man man
所以,有问题,找男人,男人如果有问题,继续找男人。
不知道找哪个男人,只记得函数里面有个单词叫timer。那么先定位,可以用类似apropos的工具:
baohua@baohua-VirtualBox:~/develop$ apropos timer
getitimer (2) - get or set value of an interval timer
IPC::Run::Timer (3pm) - - Timer channels for IPC::Run.
setitimer (2) - get or set value of an interval timer
time (7) - overview of time and timers
timer_create (2) - create a POSIX per-process timer
timer_delete (2) - delete a POSIX per-process timer
...
再man那个函数即可。
提示
欢迎单位的女程序员有问题后直接请教男同事,
请美女程序员直接忽略本文的所有条款,
美女程序员不受任何问问题规则约束 :-)
有问题,找Google
有的程序员又跳出来了,“报告,我google上不去”,相信我,一个不会翻墙的程序员,多半不会是一个好程序员。你连墙都不会翻(或者懒得翻的可能性更大),又如何查阅到最合适你的资料?
“报告,翻墙要花钱”,那么,吃饭要不要花钱?对于程序员来说,翻墙的需求,基本和吃饭的需求是一样的。翻墙越多,水平越高(翻墙看YY网站除外,那无非是多认识几个老师)。
我们不是嘲笑baidu一定不行,而是很多时候,工作的时候,碰到的kernel panic,碰到的segmentation fault,很多的其他的现场,它都是以英文的形式来呈现,很可能你在google里面能直接搜索到别人跟你碰到同样的问题。抛开baidu卖狗皮膏药的道德层面问题不谈,baidu在英文方面的搜索能力显然是远远落后google的。
下面我们搜索Kernel hard lockup,百度显示如下(应该说还不错):
Google显示如下(精准地定位到了Linux kernel官网的文档):
我们不是崇洋媚外,我们只是正视血淋淋的现实。
感恩回答者
很多程序员问别人问题,觉得别人天经地义应该回答你。而且问了很stupid的问题,别人没有好好回答,还觉得回答者在装逼。其实反过来想,一定要明白,回答我们问题的人,其实真的是在给我们工作。几年前,我在LKML问Greg K.H.下一个LTSI(工业级稳定版Linux) 版本是什么,他给我的回答非常简单直接,体现了一个正确的回答问题的风范:
“you are asking me to do work for you, right?”,你问别人问题的时候,你在让别人为你工作(而且这个工作还是免费的),对不对?
最好的问问题的方法,就是尽量不要问问题。如果要问题,要问能打动被提问者的问题。如何打动?你问问题,显示你已经经过足够的思辨。建议每一个人,可以深度学习一下《How-To-Ask-Questions-The-Smart-Way》这个文档,它是一个git项目哦:
https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way
最后的话
他回忆说:“我的名字是妈妈取的,我问过她为什么给我取这么奇怪的名字,她说是王勃的《滕王阁序》'雄州雾列,俊采星驰,台隍枕夷夏之交,宾主尽东南之美'这句来的。”
哪怕是一个龙套,我们也要认真地去做。不然就会永远是个龙套。因为,这是一个演员的自我修养。
不会问问题(意味着从来不试图自己去解决问题),我们恐怕永远是一个龙套。当你碰到一个bug,你去问别人,然后别人帮你快速解决了,自己的所学是有限的。而自己陷入思考,真正去debug,去查阅资料,去绞尽脑汁,这个过程的获得,除了这个修复bug本身以外,更可能学到了很多的知识,获取了很多的经验,提升了自己的认知。而这种认知,是你问一万个问题,并且立即获得了答案,也无法取得的。
三个耍流氓
不调试就问
无背景就问
不学习就问
三个好习惯
有问题,找男人
有问题,找Google
感恩回答者
苹果手机打赏